/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.jas.as.impl;

import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFor;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsVars;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * Finds all variable declarations and moves them to the top of
 * each block. For example, we convert this:
 * <blockquote>
 * <pre>
 *      var f = function() {
 *          y++;
 *      }
 *      var y = 17;
 * </pre>
 * </blockquote>
 * into this:
 * <blockquote>
 * <pre>
 *      var f;
 *      var y;
 *      f = function() {
 *          y++;
 *      }
 *      y = 17;
 * </pre>
 * </blockquote>
 *
 * <p>
 * This is necessary to make the program compatible with ActionScript 2.0.
 */
public final class JsVarMover {

  private static class Visitor extends JsModVisitor {

    private final Stack<List<JsName>> varLists = new Stack<List<JsName>>();
    private final Stack<Boolean> inBlock = new Stack<Boolean>();

    /**
     * Inventory all variables declared in this block and
     * save the list for later use by <code>endVisit()</code>.
     */
    @Override
    public boolean visit(JsBlock block, JsContext ctx) {
      List<JsName> varList = new ArrayList<JsName>();
      for (JsStatement stmt : block.getStatements()) {
        if (stmt instanceof JsVars) {
          for (JsVars.JsVar var : (JsVars)stmt) {
            varList.add(var.getName());
          }
        }
      }
      varLists.push(varList);
      inBlock.push(true);
      return true;
    }

    /**
     * Remove each variable declaration with no initializer, and
     * replace each variable declaration with an initializer with
     * an equivalent variable assignment.
     */
    @Override
    public void endVisit(JsVars vars, JsContext  ctx) {

      // Avoid for() statements
      if (!inBlock.peek())
        return;

      // Create initializer assignment expressions
      List<JsExpression> assignments = new ArrayList<JsExpression>();
      for (JsVars.JsVar var : vars) {
        JsExpression initExpr = var.getInitExpr();
        if (initExpr != null) {
          assignments.add(new JsBinaryOperation(var.getSourceInfo(), JsBinaryOperator.ASG,
            new JsNameRef(var.getSourceInfo(), var.getName()), initExpr));
        }
      }

      // Collect assignments into single comma separated statement
      JsExpression combined = null;
      for (JsExpression assignment : assignments) {
        combined = combined == null ? assignment :
          new JsBinaryOperation(assignment.getSourceInfo(), JsBinaryOperator.COMMA, combined, assignment);
      }

      // Replace variable declaration(s) with variable assignment(s)
      if (combined == null)
        ctx.removeMe();
      else
        ctx.replaceMe(new JsExprStmt(vars.getSourceInfo(), combined));
    }

    /**
     * Replace block with the same thing but having all the variable
     * declarations at the front.
     */
    @Override
    public void endVisit(JsBlock block, JsContext ctx) {
      List<JsName> varList = varLists.pop();
      JsVars vars = new JsVars(block.getSourceInfo());
      for (JsName name : varList) {
        vars.add(new JsVars.JsVar(vars.getSourceInfo(), name));
      }
      if (!vars.isEmpty())
        block.getStatements().add(0, vars);
      inBlock.pop();
    }

    /**
     * We want to skip var declarations within for() statements.
     */
    @Override
    public boolean visit(JsFor forstmt, JsContext ctx) {
        inBlock.push(false);
        return true;
    }

    @Override
    public void endVisit(JsFor forstmt, JsContext ctx) {
        inBlock.pop();
    }
  }

  public static void exec(JsProgram program) {
    new Visitor().accept(program);
  }

  private JsVarMover() {
  }
}
